#pragma once

#include "JSIValueConverter.h"
#include "Logger.h"
#include "QuickJSRuntime.h"
#include "jsi/jsi.h"

namespace qjs {
extern JSClassID g_hostFunctionClassID;
extern JSClassID g_hostObjectClassID;
extern JSClassID g_objectNativeStateClassID;
class HostProxy {
 protected:
  enum class ProxyType : uint8_t {
    T_HOST_OBJECT = 0,
    T_HOST_FUNCTION = 1,
    T_NATIVE_STATE = 2,
  };

  HostProxy(ProxyType type) : type_(type) {}

 public:
  bool isHostObject() const { return type_ == ProxyType::T_HOST_OBJECT; }
  bool isHostFunction() const { return type_ == ProxyType::T_HOST_FUNCTION; }
  bool hasNativeState() const { return type_ == ProxyType::T_NATIVE_STATE; }

 private:
  ProxyType type_;
};

class HostObjectProxy : public HostProxy {
 public:
  HostObjectProxy(QuickJSRuntime& runtime, std::shared_ptr<jsi::HostObject> hostObject)
      : HostProxy(ProxyType::T_HOST_OBJECT), runtime_(runtime), hostObject_(hostObject) {}

  std::shared_ptr<jsi::HostObject> getHostObject() const { return hostObject_; }

  static JSValue getProperty(JSContext* ctx, JSValueConst object, JSAtom atom, JSValueConst receiver) {
    auto hostObjectProxy = static_cast<HostObjectProxy*>(JS_GetOpaque(object, g_hostObjectClassID));
    auto& runtime = hostObjectProxy->runtime_;
    jsi::PropNameID propNameID = JSIValueConverter::ToJSIPropNameID(runtime, atom);
    jsi::Value ret;
    try {
      ret = hostObjectProxy->getHostObject()->get(runtime, propNameID);
    } catch (const jsi::JSError& error) {
      JS_Throw(ctx, JSIValueConverter::ToJSValue(runtime, error.value()));
      return JS_EXCEPTION;
    } catch (const std::exception& ex) {
      auto excValue = runtime.global()
                          .getPropertyAsFunction(runtime, "Error")
                          .call(runtime, std::string("Exception in HostFunction: ") + ex.what());
      JS_Throw(ctx, JSIValueConverter::ToJSValue(runtime, excValue));
      return JS_EXCEPTION;
    } catch (...) {
      logError("get host obj prop failed\n");
      return JS_EXCEPTION;
    }
    return JSIValueConverter::ToJSValue(runtime, ret);
  }

  static int setProperty(JSContext* ctx, JSValueConst object, JSAtom atom, JSValueConst value, JSValueConst receiver,
                         int flags) {
    auto hostObjectProxy = static_cast<HostObjectProxy*>(JS_GetOpaque(object, g_hostObjectClassID));
    auto& runtime = hostObjectProxy->runtime_;
    jsi::PropNameID propNameID = JSIValueConverter::ToJSIPropNameID(runtime, atom);
    try {
      hostObjectProxy->getHostObject()->set(runtime, propNameID, JSIValueConverter::ToJSIValue(runtime, value));
    } catch (const jsi::JSError& error) {
      JS_Throw(ctx, JSIValueConverter::ToJSValue(runtime, error.value()));
      return -1;
    } catch (const std::exception& ex) {
      auto excValue = runtime.global()
                          .getPropertyAsFunction(runtime, "Error")
                          .call(runtime, std::string("Exception in HostFunction: ") + ex.what());
      JS_Throw(ctx, JSIValueConverter::ToJSValue(runtime, excValue));
      return -1;
    } catch (...) {
      logError("set host obj prop failed\n");
      abort();
    }
    return 1;
  }

  static int getPropertyNames(JSContext* ctx, JSPropertyEnum** ptab, uint32_t* plen, JSValueConst object) {
    auto hostObjectProxy = static_cast<HostObjectProxy*>(JS_GetOpaque(object, g_hostObjectClassID));
    auto& runtime = hostObjectProxy->runtime_;
    auto names = hostObjectProxy->getHostObject()->getPropertyNames(runtime);
    JSPropertyEnum* tab;
    size_t len = names.size();
    *ptab = NULL;
    *plen = 0;
    if (len > 0) {
      tab = (JSPropertyEnum*)js_malloc(ctx, sizeof(JSPropertyEnum) * len);
      if (!tab) return -1;
      for (int i = 0; i < len; i++) {
        tab[i].is_enumerable = true;
        tab[i].atom = JS_NewAtom(ctx, names[i].utf8(runtime).c_str());
      }
      *plen = (uint32_t)len;
      *ptab = tab;
    }
    return 0;
  }

  static int getOwnProperty(JSContext* ctx, JSPropertyDescriptor* desc, JSValueConst obj, JSAtom prop) {
    desc->value = JS_AtomToValue(ctx, prop);
    desc->flags = JS_PROP_ENUMERABLE;
    return 1;
  }

  static void finalize(JSRuntime* rt, JSValue val) {
    auto hostObject = static_cast<HostObjectProxy*>(JS_GetOpaque(val, g_hostObjectClassID));
    JS_SetOpaque(val, nullptr);
    delete hostObject;
  }

 private:
  QuickJSRuntime& runtime_;
  std::shared_ptr<jsi::HostObject> hostObject_;
};

class ObjectNativeState : public HostProxy {
 public:
  ObjectNativeState(QuickJSRuntime& runtime, std::shared_ptr<jsi::NativeState> nativeState)
      : HostProxy(ProxyType::T_NATIVE_STATE), runtime_(runtime), nativeState_(nativeState) {}

  std::shared_ptr<jsi::NativeState> getNativeState() const { return nativeState_; }

  static void finalize(JSRuntime* rt, JSValue val) {
    auto nativeState = static_cast<ObjectNativeState*>(JS_GetOpaque(val, g_objectNativeStateClassID));
    JS_SetOpaque(val, nullptr);
    delete nativeState;
  }

 private:
  QuickJSRuntime& runtime_;
  std::shared_ptr<jsi::NativeState> nativeState_;
};

class HostFunctionProxy : public HostProxy {
 public:
  HostFunctionProxy(QuickJSRuntime& runtime, jsi::HostFunctionType funcType)
      : HostProxy(ProxyType::T_HOST_FUNCTION), runtime_(runtime), funcType_(funcType) {}

  jsi::HostFunctionType& getHostFunctionType() { return funcType_; }

  static JSValue call(JSContext* ctx, JSValueConst func_obj, JSValueConst this_val, int argc, JSValueConst* argv,
                      int flags) {
    HostFunctionProxy* hostFunctionProxy =
        static_cast<HostFunctionProxy*>(JS_GetOpaque(func_obj, g_hostFunctionClassID));
    QuickJSRuntime& rt = hostFunctionProxy->runtime_;
    jsi::Value stackArgs[argc];
    jsi::Value* args;
    for (size_t i = 0; i < argc; i++) {
      stackArgs[i] = JSIValueConverter::ToJSIValue(rt, argv[i]);
    }
    args = stackArgs;
    JSValue res;
    jsi::Value thisVal = JSIValueConverter::ToJSIValue(rt, this_val);
    try {
      res = JSIValueConverter::ToJSValue(rt, hostFunctionProxy->funcType_(rt, thisVal, args, argc));
    } catch (const std::exception& ex) {
      auto excValue = rt.global()
                          .getPropertyAsFunction(rt, "Error")
                          .call(rt, std::string("Exception in HostFunction: ") + ex.what());
      JS_Throw(ctx, JSIValueConverter::ToJSValue(rt, excValue));
      return JS_EXCEPTION;
    } catch (const std::string& ex) {
      auto excValue =
          rt.global().getPropertyAsFunction(rt, "Error").call(rt, std::string("Exception in HostFunction: ") + ex);
      JS_Throw(ctx, JSIValueConverter::ToJSValue(rt, excValue));
      return JS_EXCEPTION;
    } catch (...) {
      logError("hostFunctionProxy failed!\n");
      res = JS_UNDEFINED;
    }
    return res;
  }

  static void finalize(JSRuntime* rt, JSValue val) {
    HostFunctionProxy* hostFunctionProxy = static_cast<HostFunctionProxy*>(JS_GetOpaque(val, g_hostFunctionClassID));
    JS_SetOpaque(val, nullptr);
    delete hostFunctionProxy;
  }

 private:
  QuickJSRuntime& runtime_;
  jsi::HostFunctionType funcType_;
};

}  // namespace qjs
